package ca.benow.transmission;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;

import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
import org.json.mine.JSONArray;
import org.json.mine.JSONException;
import org.json.mine.JSONObject;
import org.json.mine.JSONTokener;

import android.content.Context;
import android.net.http.AndroidHttpClient;
import android.util.Log;
import ca.benow.transmission.model.AddedTorrentInfo;
import ca.benow.transmission.model.SessionStatus;
import ca.benow.transmission.model.TorrentStatus;
import ca.benow.transmission.model.TorrentStatus.TorrentField;
import ca.benow.transmission.model.TrackerPair;
import ca.benow.transmission.model.TransmissionSession;
import ca.benow.transmission.model.TransmissionSession.SessionField;
import ca.benow.transmission.model.TransmissionSession.SessionPair;

import com.mystnihon.freeremote.util.Constants;
import com.mystnihon.freeremote.util.NetworkConnection;
import com.mystnihon.freeremote.util.NetworkConnection.HttpMethod;
import com.mystnihon.freeremote.util.exception.NFRestClientException;
import com.mystnihon.freeremote.util.NetworkConnectionResult;

/**
 * The main class for interacting with transmission. Create an instance with
 * appropriate parameters and call the methods. If possible, re-use the instance
 * to avoid slight network overhead.
 * <p/>
 * Here's an example.
 * 
 * <pre>
 * // initialize log4j
 * DOMConfigurator.configure(&quot;etc/logging.xml&quot;);
 * 
 * TransmissionClient client = new TransmissionClient(new URL(&quot;http://transmission:transmission@localhost:9091/transmission/rpc&quot;));
 * List&lt;TorrentStatus&gt; torrents = client.getAllTorrents();
 * for (TorrentStatus curr : torrents)
 * 	System.out.println(curr);
 * </pre>
 * 
 * By tuning the log4j config, it is possible to see the JSON RPC request and
 * response text, which might be useful for debugging.
 * 
 * @author andy
 */
public class TransmissionClient {

	private static final String TAG = TransmissionClient.class.getSimpleName();
	private static final String ID_RECENTLY_ACTIVE = "recently-active";
	private static final String SESSION_HEADER = "X-Transmission-Session-Id";
	private static final int DEFAULT_PORT = 9091;
	// private static final Logger log =
	// Logger.getLogger(TransmissionClient.class);

	public static final int PRIORITY_LOW = -1;
	public static final int PRIORITY_NORMAL = 0;
	public static final int PRIORITY_HIGH = 1;

	private final URI url;
	private String userCrypt;
	private String sessionId;
	private static int tag = 0;
	private NetworkConnection hconn;
	private Context mContext;

	/**
	 * Creates a new client that connects to a given url. URL should be
	 * something similar to
	 * 
	 * <pre>
	 * http://transmission:transmission@localhost:9091/transmission/rpc
	 * </pre>
	 * 
	 * @param uri
	 */
	public TransmissionClient(Context context, URI uri) {
		this.url = uri;
		if (uri.getUserInfo() != null) {
			String uinfo = uri.getUserInfo();
			if (uinfo != null)
				userCrypt = Base64.encode(uinfo);
		}
	}

	/**
	 * Creates a new client that connects to a given host on a given port with
	 * the given user and password.
	 * 
	 * @param host
	 *            host to connect to
	 * @param port
	 *            port to connect on
	 * @param user
	 *            user to connect as
	 * @param pass
	 *            password for user
	 */
	public TransmissionClient(Context context, String host, int port, String user, String pass) {
		this(context, createURL(host, port, user, pass));
	}

	/**
	 * Creates a new client that connects to a given host on the default port
	 * (9091) with the given user and pass
	 * 
	 * @param host
	 *            host to connect to
	 * @param user
	 *            user to connect as
	 * @param pass
	 *            password for user
	 */
	public TransmissionClient(Context context, String host, String user, String pass) {
		this(context, createURL(host, DEFAULT_PORT, user, pass));
	}

	/**
	 * Create a new client that connects to a given host on the default port
	 * (9091) with the default user/pass (transmission/transmission)
	 * 
	 * @param host
	 *            host to connect to
	 */
	public TransmissionClient(Context context, String host) {
		this(context, host, DEFAULT_PORT);
	}

	/**
	 * Create a new client that connects to given host and port with default
	 * user/pass (transmission/transmission)
	 * 
	 * @param host
	 *            host to connect to
	 * @param port
	 *            port to connect on
	 */
	public TransmissionClient(Context context, String host, int port) {
		this(context, host, port, "transmission", "transmission");
	}

	/**
	 * Creates a new client that connects to the local transmission using
	 * default parameters
	 */
	public TransmissionClient(Context context) {
		this(context, createURL("localhost", 9091, "transmission", "transmission"));
	}

	private static URI createURL(String host, int port, String user, String pass) {
		try {
			String url = "http://" + host + ":" + port + "/transmission/rpc";
			// String url = "http://"
			// + (user == null ? "" : user + ":" + URLEncoder.encode(pass) +
			// "@") + host
			// + ":" + port + "/transmission/rpc";
			// URL ur = new URL(url);
			URI od = new URI("http", (user == null ? "" : user + ":" + pass), host, port, "/transmission/rpc", null, null);
			return od;
		} catch (URISyntaxException e) {
			throw new RuntimeException("URISyntaxException Souci, again. ", e);
		}
	}
	AndroidHttpClient client;
	/**
	 * Send a command to Transmission.
	 * 
	 * @param name
	 *            name of command, which forms the 'method' in request
	 * @param args
	 *            arguments which for body of 'arguments' in request
	 * @return json object containing result of 'arguments' in response, if any
	 * @throws IOException
	 *             on problem communicating
	 * @throws TransmissionException
	 *             on Transmission problem when performing the command
	 */

	public JSONObject sendCommand(String name, JSONObject args) throws IOException {
		AndroidHttpClient client = AndroidHttpClient.newInstance(Constants.USER_AGENT);
		hconn = new NetworkConnection(client);
		JSONObject resultArgs = null;
		final ArrayList<Header> headers = new ArrayList<Header>();
		if (userCrypt != null)
			headers.add(new BasicHeader("Authorization", "Basic " + userCrypt));
		if (sessionId != null)
			headers.add(new BasicHeader(SESSION_HEADER, sessionId));

		JSONObject command = new JSONObject();
		command.put("method", name);
		command.put("arguments", args);
		command.put("tag", "" + tag++);

		String json = command.toString(2);

		NetworkConnectionResult resultat;
		try {
			resultat = hconn.retrieveStringAndHeadersFromService(url.toString(), HttpMethod.Post, null, json + "\r\n\r\n", headers, false, "", false, mContext);

			if (resultat.mResponse.getStatusLine().getStatusCode() == 409) {
				String sessId = resultat.mResponse.getLastHeader(SESSION_HEADER).getValue();
				if (sessId != null) {
					// Reconnecting with new session id
					this.sessionId = sessId;
					return sendCommand(name, args);
				}
			}
			JSONObject result;
			// if (Config.DEBUG_MODE_ENABLED) {
			// Log.d("JSON-Response", String.format("Mesage %s", msg));
			// }
			JSONTokener toker = new JSONTokener(resultat.mResult);
			result = new JSONObject(toker);

			// if (log.isDebugEnabled())
			// log.debug("Read:\n" + result.toString(2));

			String resultStr = result.getString("result");
			if (!resultStr.equals("success"))
				throw new TransmissionException(resultStr, command.toString(2), resultat.mResult);

			if (result.has("arguments"))
				resultArgs = result.getJSONObject("arguments");

		} catch (IllegalStateException e1) {
			Log.w(TAG, "IllegalStateException", e1);
			throw new IOException(e1);
		} catch (URISyntaxException e1) {
			Log.w(TAG, "URISyntaxException", e1);
		} catch (NFRestClientException e1) {
			Log.w(TAG, "NFRestClientException", e1);
		} catch (JSONException e) {
			// Error parsing json
			throw e;
		} finally {
			client.close();
		}

		return resultArgs;
	}

	/**
	 * Get status of torrents
	 * 
	 * @param ids
	 *            optional ids of torrents to fetch status for, if not given,
	 *            status for all torrents will be fetched
	 * @param requestedFields
	 *            information fields to fetch, if not given, only the id and
	 *            name fields are fetched
	 * @return status for requested torrents
	 * @throws IOException
	 */
	public ArrayList<TorrentStatus> getTorrents(int[] ids, TorrentStatus.TorrentField[] requestedFields) throws IOException {
		JSONObject args = new JSONObject();
		if (ids != null && ids.length > 0) {
			JSONArray idAry = new JSONArray();
			for (int i = 0; i < ids.length; i++)
				idAry.put(ids[i]);
			args.put("ids", idAry);
		}
		if (requestedFields == null)
			requestedFields = TorrentStatus.defaultFields;
		else {
			if (requestedFields.length > 0) {
				boolean hasAll = false;
				for (int i = 0; i < requestedFields.length; i++) {
					if (requestedFields[i].equals(TorrentField.all))
						hasAll = true;
				}
				if (hasAll) {
					requestedFields = new TorrentField[TorrentField.values().length - 1];
					for (int i = 0; i < TorrentField.values().length; i++)
						requestedFields[i] = TorrentField.values()[i + 1];
				}
			}
		}
		JSONArray fields = new JSONArray();
		for (int i = 0; i < requestedFields.length; i++)
			fields.put(TorrentStatus.fieldNameByFieldPos[requestedFields[i].ordinal()]);
		args.put("fields", fields);

		ArrayList<TorrentStatus> torrents = new ArrayList<TorrentStatus>();
		JSONObject result = sendCommand("torrent-get", args);
		JSONArray torAry = result.getJSONArray("torrents");
		for (int i = 0; i < torAry.length(); i++)
			torrents.add(new TorrentStatus(torAry.getJSONObject(i)));

		return torrents;
	}

	public ArrayList<TorrentStatus> getAllTorrents(TorrentField[] torrentFields) throws IOException {
		return getTorrents(null, torrentFields);
	}

	public ArrayList<TorrentStatus> getAllTorrents() throws IOException {
		return getTorrents(null, null);
	}

	/**
	 * Adds a new torrent by name or url
	 * 
	 * @param downloadDir
	 *            path to download the torrent to
	 * @param torrentFileNameOrURL
	 *            local filename or url to torrent
	 * @param paused
	 *            if true, don't start the torrent
	 * @param peerLimit
	 *            maximum number of peers
	 * @param bandwidthPriority
	 *            one of the PRIORITY_* constants
	 * @param filesWanted
	 *            indices of file(s) to download
	 * @param filesUnwanted
	 *            indices of file(s) to not download
	 * @param priorityHigh
	 *            indices of high-priority file(s)
	 * @param priorityLow
	 *            indices of low-priority file(s)
	 * @param priorityNormal
	 *            indices of normal-priority file(s)
	 * @return info about added torrent
	 * @throws IOException
	 */
	public AddedTorrentInfo addTorrent(String downloadDir, String torrentFileNameOrURL, boolean paused, int peerLimit, int bandwidthPriority, int[] filesWanted, int[] filesUnwanted,
			int[] priorityHigh, int[] priorityLow, int[] priorityNormal) throws IOException {
		return addTorrent(downloadDir, torrentFileNameOrURL, null, paused, peerLimit, bandwidthPriority, filesWanted, filesUnwanted, priorityHigh, priorityLow, priorityNormal);
	}

	/**
	 * Adds a new torrent directly from torrent data
	 * 
	 * @param downloadDir
	 *            path to download the torrent to
	 * @param metaInfo
	 *            inputstream to torrent contents (will be encoded)
	 * @param paused
	 *            if true, don't start the torrent
	 * @param peerLimit
	 *            maximum number of peers
	 * @param bandwidthPriority
	 *            one of the PRIORITY_* constants
	 * @param filesWanted
	 *            indices of file(s) to download
	 * @param filesUnwanted
	 *            indices of file(s) to not download
	 * @param priorityHigh
	 *            indices of high-priority file(s)
	 * @param priorityLow
	 *            indices of low-priority file(s)
	 * @param priorityNormal
	 *            indices of normal-priority file(s)
	 * @return info about added torrent
	 * @throws IOException
	 */
	public AddedTorrentInfo addTorrent(String downloadDir, InputStream metaInfo, boolean paused, int peerLimit, int bandwidthPriority, int[] filesWanted, int[] filesUnwanted, int[] priorityHigh,
			int[] priorityLow, int[] priorityNormal) throws IOException {
		return addTorrent(downloadDir, null, metaInfo, paused, peerLimit, bandwidthPriority, filesWanted, filesUnwanted, priorityHigh, priorityLow, priorityNormal);
	}

	private AddedTorrentInfo addTorrent(String downloadDir, String torrentFileNameOrURL, InputStream metaInfo, boolean paused, int peerLimit, int bandwidthPriority, int[] filesWanted,
			int[] filesUnwanted, int[] priorityHigh, int[] priorityLow, int[] priorityNormal) throws IOException {
		JSONObject obj = new JSONObject();
		if (downloadDir != null)
			obj.put("download-dir", downloadDir);
		if (torrentFileNameOrURL == null && metaInfo == null)
			throw new NullPointerException("A torrentFileNameOrURL or metaInfo parameter is required");
		if (torrentFileNameOrURL != null)
			obj.put("filename", torrentFileNameOrURL);
		if (metaInfo != null) {
			ByteArrayOutputStream out = new ByteArrayOutputStream();
			encode(metaInfo, out);
			String encoded = new String(out.toByteArray());
			obj.put("metainfo", encoded);
		}
		obj.put("paused", paused);
		if (peerLimit >= 0)
			obj.put("peer-limit", peerLimit);
		if (bandwidthPriority > 0) {
			obj.put("bandwidthPriority", bandwidthPriority);
		}

		if (filesWanted != null && filesWanted.length > 0)
			obj.put("files-wanted", new JSONArray(filesWanted));
		if (filesUnwanted != null && filesUnwanted.length > 0)
			obj.put("files-unwanted", new JSONArray(filesUnwanted));
		if (priorityHigh != null && priorityHigh.length > 0)
			obj.put("priority-high", new JSONArray(priorityHigh));
		if (priorityLow != null && priorityLow.length > 0)
			obj.put("priority-low", new JSONArray(priorityLow));
		if (priorityNormal != null && priorityNormal.length > 0)
			obj.put("priority-normal", new JSONArray(priorityNormal));

		JSONObject result = sendCommand("torrent-add", obj);
		return new AddedTorrentInfo(result.getJSONObject("torrent-added"));
	}

	public void encode(InputStream in, OutputStream out) throws IOException {
		ByteArrayOutputStream tout = new ByteArrayOutputStream();
		byte[] buff = new byte[512];
		int read = in.read(buff);
		while (read > 0) {
			tout.write(buff, 0, read);
			read = in.read(buff);
		}
		byte[] result = android.util.Base64.encode(tout.toByteArray(), android.util.Base64.DEFAULT);
		out.write(result);
		out.flush();
		out.close();
	}

	/**
	 * Start given torrents
	 * 
	 * @param ids
	 *            numerical ids, string hashes or the ID_RECENTLY_ADDED constant
	 * @throws IOException
	 */
	public void startTorrents(Object... ids) throws IOException {
		if (ids == null)
			throw new NullPointerException("At least one id is required");
		JSONObject obj = new JSONObject();
		if (ids.length == 1)
			obj.put("ids", ids[0]);
		else {
			JSONArray ary = new JSONArray();
			for (int i = 0; i < ids.length; i++)
				ary.put(ids[i]);
			obj.put("ids", ary);
		}
		sendCommand("torrent-start", obj);
	}

	public void startAllTorrents() throws IOException {
		JSONObject obj = new JSONObject();
		sendCommand("torrent-start", obj);
	}

	/**
	 * Stop given torrents
	 * 
	 * @param ids
	 *            numerical ids, string hashes or the ID_RECENTLY_ADDED constant
	 * @throws IOException
	 */
	public void stopTorrents(Object... ids) throws IOException {
		if (ids == null)
			throw new NullPointerException("At least one id is required");
		JSONObject obj = new JSONObject();
		if (ids.length == 1)
			obj.put("ids", ids[0]);
		else {
			JSONArray ary = new JSONArray();
			for (int i = 0; i < ids.length; i++)
				ary.put(ids[i]);
			obj.put("ids", ary);
		}
		sendCommand("torrent-stop", obj);
	}

	public void stopAllTorrents() throws IOException {
		JSONObject obj = new JSONObject();
		sendCommand("torrent-stop", obj);
	}

	/**
	 * Verify given torrents
	 * 
	 * @param ids
	 *            numerical ids, string hashes or the ID_RECENTLY_ADDED constant
	 * @throws IOException
	 */
	public void verifyTorrents(Object... ids) throws IOException {
		if (ids == null)
			throw new NullPointerException("At least one id is required");
		JSONObject obj = new JSONObject();
		if (ids.length == 1)
			obj.put("ids", ids[1]);
		else {
			JSONArray ary = new JSONArray();
			for (int i = 0; i < ids.length; i++)
				ary.put(ids[i]);
			obj.put("ids", ary);
		}
		sendCommand("torrent-verify", obj);
	}

	/**
	 * Reannounce (fetch new peers) for given torrents
	 * 
	 * @param ids
	 *            numerical ids, string hashes or the ID_RECENTLY_ADDED constant
	 * @throws IOException
	 */
	public void reannounceTorrents(Object... ids) throws IOException {
		if (ids == null)
			throw new NullPointerException("At least one id is required");
		JSONObject obj = new JSONObject();
		if (ids.length == 1)
			obj.put("ids", ids[1]);
		else {
			JSONArray ary = new JSONArray();
			for (int i = 0; i < ids.length; i++)
				ary.put(ids[i]);
			obj.put("ids", ary);
		}
		sendCommand("torrent-reannounce", obj);
	}

	public void reannounceRecentlyActiveTorrents() throws IOException {
		JSONObject obj = new JSONObject();
		obj.put("ids", TransmissionClient.ID_RECENTLY_ACTIVE);
		sendCommand("torrent-reannounce", obj);
	}

	/**
	 * Sets properties of selected torrents
	 * 
	 * @param ids
	 *            numerical ids, string hashes or the ID_RECENTLY_ADDED
	 *            constant. If null all ids will be affected
	 * @param bandwidthPriority
	 *            this torrent's bandwidth tr_priority_t
	 * @param downloadLimit
	 *            maximum download speed (KBps)
	 * @param downloadLimited
	 *            true if "downloadLimit" is honored
	 * @param filesWanted
	 *            indices of file(s) to download. Empty array for all indices.
	 * @param filesUnwanted
	 *            indices of file(s) to not download. Empty array for all
	 *            indices.
	 * @param honorsSessionLimits
	 *            true if session upload limits are honored
	 * @param location
	 *            new location of the torrent's content
	 * @param peerLimit
	 *            maximum number of peers
	 * @param priorityHigh
	 *            indices of high-priority file(s). Empty array for all indices.
	 * @param priorityLow
	 *            indices of low-priority file(s). Empty array for all indices.
	 * @param priorityNormal
	 *            indices of normal-priority file(s). Empty array for all
	 *            indices.
	 * @param seedIdleLimit
	 *            torrent-level number of minutes of seeding inactivity
	 * @param seedIdleMode
	 *            which seeding inactivity to use. See tr_inactvelimit
	 * @param seedRatioLimit
	 *            torrent-level seeding ratio
	 * @param seedRatioMode
	 *            which ratio to use. See tr_ratiolimit
	 * @param trackerAdd
	 *            strings of announce URLs to addd
	 * @param trackerRemove
	 *            ids of trackers to remove
	 * @param trackerReplace
	 *            pairs of <trackerId/new announce URLs>
	 * @param uploadLimit
	 *            maximum upload speed (KBps)
	 * @param uploadLimited
	 *            true if "uploadLimit" is honored
	 * @throws IOException
	 */
	public void setTorrents(Object[] ids, int bandwidthPriority, int downloadLimit, boolean downloadLimited, int[] filesWanted, int[] filesUnwanted, boolean honorsSessionLimits, String location,
			int peerLimit, int[] priorityHigh, int[] priorityLow, int[] priorityNormal, int seedIdleLimit, int seedIdleMode, double seedRatioLimit, int seedRatioMode, String[] trackerAdd,
			int[] trackerRemove, TrackerPair[] trackerReplace, int uploadLimit, boolean uploadLimited) throws IOException {
		JSONObject obj = new JSONObject();
		if (ids != null && ids.length == 1)
			obj.put("ids", ids[1]);
		else {
			JSONArray ary = new JSONArray();
			if (ids != null) {
				for (int i = 0; i < ids.length; i++)
					ary.put(ids[i]);
			}
			obj.put("ids", ary);
		}
		obj.put("bandwidthPriority", bandwidthPriority);
		obj.put("downloadLimit", downloadLimit);
		obj.put("downloadLimited", downloadLimited);
		if (filesWanted != null) {
			JSONArray ary = new JSONArray();
			for (int i = 0; i < filesWanted.length; i++)
				ary.put(filesWanted[i]);
			obj.put("files-wanted", ary);
		}
		if (filesUnwanted != null) {
			JSONArray ary = new JSONArray();
			for (int i = 0; i < filesUnwanted.length; i++)
				ary.put(filesUnwanted[i]);
			obj.put("files-unwanted", ary);
		}
		obj.put("honorsSessionLimits", honorsSessionLimits);
		if (location != null)
			obj.put("location", location);
		obj.put("peer-limit", peerLimit);
		if (priorityHigh != null) {
			JSONArray ary = new JSONArray();
			for (int i = 0; i < priorityHigh.length; i++)
				ary.put(priorityHigh[i]);
			obj.put("priority-high", ary);
		}
		if (priorityLow != null) {
			JSONArray ary = new JSONArray();
			for (int i = 0; i < priorityLow.length; i++)
				ary.put(priorityLow[i]);
			obj.put("priority-low", ary);
		}
		if (priorityNormal != null) {
			JSONArray ary = new JSONArray();
			for (int i = 0; i < priorityNormal.length; i++)
				ary.put(priorityNormal[i]);
			obj.put("priority-normal", ary);
		}

		obj.put("seedIdleLimit", seedIdleLimit);
		obj.put("seedIdleMode", seedIdleMode);
		obj.put("seedRatioLimit", seedRatioLimit);
		obj.put("seedRatioMode", seedRatioMode);
		if (trackerAdd != null) {
			JSONArray ary = new JSONArray();
			for (int i = 0; i < trackerAdd.length; i++)
				ary.put(trackerAdd[i]);
			obj.put("trackerAdd", ary);
		}
		if (trackerRemove != null) {
			JSONArray ary = new JSONArray();
			for (int i = 0; i < trackerRemove.length; i++)
				ary.put(trackerRemove[i]);
			obj.put("trackerRemove", ary);
		}
		if (trackerReplace != null) {
			JSONArray ary = new JSONArray();
			for (int i = 0; i < trackerReplace.length; i++) {
				JSONArray ary2 = new JSONArray();
				ary2.put(trackerReplace[i].id);
				ary2.put(trackerReplace[i].newURL);
				ary.put(ary2);
			}
			obj.put("trackerReplace", ary);
		}
		obj.put("uploadLimit", uploadLimit);
		obj.put("uploadLimited", uploadLimited);

		sendCommand("torrent-set", obj);
	}

	/**
	 * Removes given torrents
	 * 
	 * @param ids
	 *            numerical ids, string hashes or the ID_RECENTLY_ADDED constant
	 * @param deleteLocalData
	 * @throws IOException
	 */
	public void removeTorrents(Object[] ids, boolean deleteLocalData) throws IOException {
		if (ids == null)
			throw new NullPointerException("At least one id is required");
		JSONObject obj = new JSONObject();
		if (ids.length == 1)
			obj.put("ids", ids[0]);
		else {
			JSONArray ary = new JSONArray();
			for (int i = 0; i < ids.length; i++)
				ary.put(ids[i]);
			obj.put("ids", ary);
		}
		obj.put("delete-local-data", deleteLocalData);
		sendCommand("torrent-remove", obj);
	}

	/**
	 * @param ids
	 *            numerical ids, string hashes or the ID_RECENTLY_ADDED constant
	 * @param location
	 * @param move
	 * @throws IOException
	 */
	public void moveTorrents(Object[] ids, String location, boolean move) throws IOException {
		if (ids == null)
			throw new NullPointerException("At least one id is required");
		JSONObject obj = new JSONObject();
		if (ids.length == 1)
			obj.put("ids", ids[0]);
		else {
			JSONArray ary = new JSONArray();
			for (int i = 0; i < ids.length; i++)
				ary.put(ids[i]);
			obj.put("ids", ary);
		}
		obj.put("location", location);
		obj.put("move", move);
		sendCommand("torrent-set-location", obj);
	}

	private static SessionField[] SET_SESSION_DISALLOWED = new TransmissionSession.SessionField[] { SessionField.blocklistSize, SessionField.configDir, SessionField.rpcVersion,
			SessionField.rpcVersionMinimum, SessionField.version };

	/**
	 * @param pairs
	 *            one or more pair of TransmissionSession.SessionField and
	 *            value. All SessionFields are valid, except: blocklistSize,
	 *            configDir, rpcVersion, rpcVersionMinimum, and version. An
	 *            error will be surfaced if those are included
	 * @throws IOException
	 */
	public void setSession(SessionPair... pairs) throws IOException {
		if (pairs == null)
			throw new NullPointerException("At least one pair is required");
		JSONObject obj = new JSONObject();
		for (int i = 0; i < pairs.length; i++) {
			SessionField curr = pairs[i].field;
			for (int j = 0; j < SET_SESSION_DISALLOWED.length; j++) {
				if (SET_SESSION_DISALLOWED[j] == curr)
					throw new IllegalArgumentException("Disallowed: " + curr.name());
			}
			obj.put(TransmissionSession.FIELD_NAMES[curr.ordinal()], pairs[i].value);
		}
		sendCommand("session-set", obj);
	}

	public HashMap<SessionField, Object> getSession() throws IOException {
		JSONObject result = sendCommand("session-get", null);
		HashMap<SessionField, Object> valByField = new HashMap<SessionField, Object>();
		for (int i = 0; i < TransmissionSession.FIELD_NAMES.length; i++) {
			String curr = TransmissionSession.FIELD_NAMES[i];
			Object val = result.get(curr);
			valByField.put(SessionField.values()[i], val);
		}
		return valByField;
	}

	/**
	 * @return session status
	 * @throws IOException
	 */
	public SessionStatus getSessionStats() throws IOException {
		return new SessionStatus(sendCommand("session-stats", null));
	}

	public int updateBlocklist() throws IOException {
		return sendCommand("session-stats", null).getInt("blocklist-size");
	}

	/**
	 * This method tests to see if your incoming peer port is accessible from
	 * the outside world.
	 * 
	 * @return true if incoming peer port is accessible from outside
	 * @throws IOException
	 */
	public boolean isPortOpen() throws IOException {
		return sendCommand("port-test", null).getBoolean("port-is-open");
	}

	public void cancel() {
		// TODO Auto-generated method stub

	}

}
